﻿/* **************************************************************************
 *
 * Copyright 2007-2010 Jeff Hardy
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * *************************************************************************/

using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using IronPython.Runtime;
using IronPython.Runtime.Operations;
using Microsoft.Scripting.Hosting;
using Microsoft.Scripting.Hosting.Providers;
using System.Text.RegularExpressions;

namespace NWSGI
{
    class WsgiExecutor
    {
        private Dictionary<string, string> headers;
        private bool headersSet = false;

        private IWsgiContext context;
        private ScriptEngine engine;

        private IWsgiConfig config;
        private static readonly PythonTuple WsgiVersion = PythonOps.MakeTuple(1, 0);

        public WsgiExecutor(ScriptEngine engine, IWsgiConfig config, IWsgiContext context)
        {
            this.engine = engine;
            this.config = config;
            this.context = context;
        }

        public void Run(WsgiCallableDelegate application)
        {
            IEnumerable responses = application(GetWsgiEnvironment(), new StartResponseDelegate(StartResponse));
            ProcessResponses(responses);
        }

        /// <summary>
        /// Send the responses to the client in accordance with PEP 333.
        /// </summary>
        /// <param name="responses">An IEnumerable of strings containing the application's responses.</param>
        private void ProcessResponses(IEnumerable responses)
        {
            if(responses != null)
            {
                // send the file directly if needed
                if(responses is WsgiFileWrapper)
                {
                    WsgiFileWrapper wrapper = (WsgiFileWrapper)responses;

                    SetHeaders();
                    context.Response.TransmitFile(wrapper.FullPath);
                }
                else
                {
                    // Gather the responses and write them
                    foreach(object responseObj in responses)
                    {
                        string responseStr = responseObj as string;
                        if(responseStr != null)
                        {
                            Write(responseStr);
                        }
                    }
                }
            }

            // If no response came from the app, write nothing to ensure headers are sent
            if(!headersSet)
                Write("");

            // If the object has a 'close' method, it must be called
            object closeMethod = null;
            if(engine.Operations.TryGetMember(responses, "close", out closeMethod) && engine.Operations.IsCallable(closeMethod))
            {
                engine.Operations.Invoke(closeMethod);
            }
        }

        public void Write(string body_data)
        {
            SetHeaders();

            // According to PEP 333, any strings returned from the application must be in iso-8859-1
            // encoding. For IronPython, only the lower 8 bits of each string codepoint can be used.
            byte[] data = Wsgi.Latin1.GetBytes(body_data);
            context.Response.Write(data);

            // PEP 333 says that the output should be flushed after every operation.
            // However, this breaks any IIS modules that depend on buffering.
            // Thus, we only flush if entenstions are not enabled.
            if(!config.EnableExtensions)
                context.Response.Flush();
        }

        public WriteDelegate StartResponse(string status, List response_headers, [DefaultParameterValue(null)]PythonTuple exc_info)
        {
            if(exc_info != null)
            {
                if(headersSet)
                {
                    // TODO: make sure that creating a default context here makes sense
                    ModuleContext context = new ModuleContext(new PythonDictionary(), (PythonContext)HostingHelpers.GetLanguageContext(engine));
                    throw PythonOps.MakeException(context.GlobalContext, exc_info[0], exc_info[1], exc_info[2]);
                }
            }

            this.context.Response.SetStatus(status);

            // PEP 333 says that these MUST NOT be sent from start_response, so
            // they are saved until later.
            headers = new Dictionary<string, string>();
            foreach(object headerTuple in response_headers)
            {
                PythonTuple header = headerTuple as PythonTuple;
                if(header != null && header.__len__() > 1)
                {
                    headers.Add(header[0].ToString(), header[1].ToString());
                }
            }

            return new WriteDelegate(Write);
        }

        private PythonDictionary GetWsgiEnvironment()
        {
            IWsgiResponse response = context.Response;
            IWsgiRequest request = context.Request;

            PythonDictionary environ = new PythonDictionary();
            
            environ["REQUEST_METHOD"] = request.Method;

            environ["SCRIPT_NAME"] = request.ScriptName;
            environ["PATH_INFO"] = request.PathInfo;

            environ["QUERY_STRING"] = request.QueryString;
            environ["CONTENT_TYPE"] = request.ContentType;
            environ["CONTENT_LENGTH"] = request.ContentLength;

            environ["SERVER_NAME"] = request.ServerName;
            environ["SERVER_PORT"] = request.ServerPort;
            environ["SERVER_PROTOCOL"] = request.ServerProtocol;

            foreach(KeyValuePair<string, string> header in request.Headers)
            {
                string cgiName = "HTTP_" + Regex.Replace(header.Key, "\\W", "_").ToUpperInvariant();
                // HTTP_CONTENT_TYPE && HTTP_CONTENT_LENGTH should not be included (per PEP 333)
                if(cgiName != "HTTP_CONTENT_TYPE"
                    && cgiName != "HTTP_CONTENT_LENGTH")
                {
                    environ[cgiName] = header.Value;
                }
            }

            foreach(KeyValuePair<string, string> cgivar in request.CgiServerVariables)
            {
                // HTTP_CONTENT_TYPE && HTTP_CONTENT_LENGTH should not be included (per PEP 333)
                if(cgivar.Key != "HTTP_CONTENT_TYPE" 
                    && cgivar.Key != "HTTP_CONTENT_LENGTH" 
                    && !environ.has_key(cgivar.Key)
                    && !string.IsNullOrEmpty(cgivar.Value))
                {
                    environ[cgivar.Key] = cgivar.Value;
                }
            }

            environ["wsgi.version"] = WsgiVersion;
            environ["wsgi.url_scheme"] = request.Url.Scheme;

            int contentLength;
            if(!int.TryParse(request.ContentLength, out contentLength))
                contentLength = -1;

            environ["wsgi.input"] = new WsgiInputStream(request.InputStream, contentLength);
            environ["wsgi.errors"] = this.context.Response.ErrorStream;

            environ["wsgi.multithread"] = true;
            environ["wsgi.multiprocess"] = true;
            environ["wsgi.run_once"] = false;
            environ["wsgi.file_wrapper"] = new CreateFileWrapper(delegate(PythonFile file, int block_size) { return new WsgiFileWrapper(engine, file, block_size); });

            // Because nwsgi.context allows things that aren't WSGI-compliant, it's only available
            // if extensions are enabled.
            if(config.EnableExtensions)
                environ["nwsgi.context"] = context;

            foreach(KeyValuePair<string, string> e in config.WsgiEnviron)
            {
                environ[e.Key] = e.Value;
            }

            environ["nwsgi.debug"] = config.DebugMode;

            return environ;
        }

        private void SetHeaders()
        {
            if(!headersSet && headers != null)
            {
                foreach(KeyValuePair<string, string> item in headers)
                {
                    this.context.Response.AddHeader(item.Key, item.Value);
                }

                headersSet = true;
            }
        }
    }
}
